﻿using log4net;
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Security;
using System.Text;

namespace Velocity4Net.Util.Introspection
{

    /*
     * Licensed to the Apache Software Foundation (ASF) under one
     * or more contributor license agreements.  See the NOTICE file
     * distributed with this work for additional information
     * regarding copyright ownership.  The ASF licenses this file
     * to you under the Apache License, Version 2.0 (the
     * "License"); you may not use this file except in compliance
     * with the License.  You may obtain a copy of the License at
     *
     *   http://www.apache.org/licenses/LICENSE-2.0
     *
     * Unless required by applicable law or agreed to in writing,
     * software distributed under the License is distributed on an
     * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
     * KIND, either express or implied.  See the License for the
     * specific language governing permissions and limitations
     * under the License.
     */









    /**
     * A cache of introspection information for a specific class instance.
     * Keys {@link java.lang.reflect.Method} objects by a concatenation of the
     * method name and the names of classes that make up the parameters.
     *
     * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
     * @author <a href="mailto:bob@werken.com">Bob McWhirter</a>
     * @author <a href="mailto:szegedia@freemail.hu">Attila Szegedi</a>
     * @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a>
     * @author <a href="mailto:henning@apache.org">Henning P. Schmiedehausen</a>
     * @author Nathan Bubna
     * @author <a href="mailto:claude.brisson@gmail.com">Claude Brisson</a>
     * @version $Id: ClassMap.java 1855206 2019-03-11 11:09:53Z cbrisson $
     */
    public class ClassMap
    {
        /** Set true if you want to debug the reflection code */
        private const bool debugReflection = false;

        /** Class logger */
        private readonly ILog log;

        /**
         * Class passed into the constructor used to as
         * the basis for the Method map.
         */
        private Type clazz;

        private readonly MethodCache methodCache;

        /**
         * Standard constructor
         * @param clazz The class for which this ClassMap gets constructed.
         * @param log logger
         */
        public ClassMap(Type clazz, ILog log) : this(clazz, log, null)
        {
        }

        /**
         * Standard constructor
         * @param clazz The class for which this ClassMap gets constructed.
         * @param log logger
         * @param conversionHandler conversion handler
         * @since 2.0
         */
        public ClassMap(Type clazz, ILog log, TypeConversionHandler conversionHandler)
        {
            this.clazz = clazz;
            this.log = log;

            if (debugReflection)
            {
                log.Debug("=================================================================");
                log.Debug("== Class: {}", clazz);
            }

            methodCache = createMethodCache(conversionHandler);

            if (debugReflection)
            {
                log.Debug("=================================================================");
            }
        }

        /**
         * Returns the class object whose methods are cached by this map.
         *
         * @return The class object whose methods are cached by this map.
         */
        public Class getCachedClass()
        {
            return clazz;
        }

        /**
         * Find a Method using the method name and parameter objects.
         *
         * @param name The method name to look up.
         * @param params An array of parameters for the method.
         * @return A Method object representing the method to invoke or null.
         * @throws MethodMap.AmbiguousException When more than one method is a match for the parameters.
         */
        public Method findMethod(String name, Object[] _params)
        {
            return methodCache.get(name, _params);
        }

        /**
         * Populate the Map of direct hits. These
         * are taken from all the public methods
         * that our class, its parents and their implemented interfaces provide.
         */
        private MethodCache createMethodCache(TypeConversionHandler conversionHandler)
        {
            MethodCache methodCache = new MethodCache(log, conversionHandler);
            //
            // Looks through all elements in the class hierarchy. This one is bottom-first (i.e. we start
            // with the actual declaring class and its interfaces and then move up (superclass etc.) until we
            // hit java.lang.Object. That is important because it will give us the methods of the declaring class
            // which might in turn be abstract further up the tree.
            //
            // We also ignore all SecurityExceptions that might happen due to SecurityManager restrictions (prominently
            // hit with Tomcat 5.5).
            //
            // We can also omit all that complicated getPublic, getAccessible and upcast logic that the class map had up
            // until Velocity 1.4. As we always reflect all elements of the tree (that's what we have a cache for), we will
            // hit the public elements sooner or later because we reflect all the public elements anyway.
            //
            // Ah, the miracles of Java for(;;) ...
            for (Class classToReflect = getCachedClass(); classToReflect != null; classToReflect = classToReflect.getSuperclass())
            {
                if (Modifier.isPublic(classToReflect.getModifiers()))
                {
                    populateMethodCacheWith(methodCache, classToReflect);
                }
                Class[] interfaces = classToReflect.getInterfaces();
                foreach (Class anInterface in interfaces)
                {
                    populateMethodCacheWithInterface(methodCache, anInterface);
                }
            }
            // return the already initialized cache
            return methodCache;
        }

        /* recurses up interface heirarchy to get all super interfaces (VELOCITY-689) */
        private void populateMethodCacheWithInterface(MethodCache methodCache, Class iface)
        {
            if (Modifier.isPublic(iface.getModifiers()))
            {
                populateMethodCacheWith(methodCache, iface);
            }
            Class[] supers = iface.getInterfaces();
            foreach (Class aSuper in supers)
            {
                populateMethodCacheWithInterface(methodCache, aSuper);
            }
        }

        private void populateMethodCacheWith(MethodCache methodCache, Class classToReflect)
        {
            if (debugReflection)
            {
                log.Debug("Reflecting {}", classToReflect);
            }

            try
            {
                Method[] methods = classToReflect.getDeclaredMethods();
                foreach (Method method in methods)
                {
                    int modifiers = method.getModifiers();
                    if (Modifier.isPublic(modifiers))
                    {
                        methodCache.put(method);
                    }
                }
            }
            catch (SecurityException se) // Everybody feels better with...
            {
                log.Debug("While accessing methods of {}:", classToReflect, se);
            }
        }

        /**
         * This is the cache to store and look up the method information.
         *
         * @author <a href="mailto:henning@apache.org">Henning P. Schmiedehausen</a>
         * @version $Id: ClassMap.java 1855206 2019-03-11 11:09:53Z cbrisson $
         */
        private sealed class MethodCache
        {
            private readonly Object CACHE_MISS = new Object();

            private readonly String NULL_ARG = nameof(Object);

            private readonly Map convertPrimitives = new HashMap();

            static
        {
            convertPrimitives.put(Boolean.TYPE, Boolean.class.getName());
            convertPrimitives.put(Byte.TYPE, Byte.class.getName());
            convertPrimitives.put(Character.TYPE, Character.class.getName());
            convertPrimitives.put(Double.TYPE, Double.class.getName());
            convertPrimitives.put(Float.TYPE, Float.class.getName());
            convertPrimitives.put(Integer.TYPE, Integer.class.getName());
            convertPrimitives.put(Long.TYPE, Long.class.getName());
            convertPrimitives.put(Short.TYPE, Short.class.getName());
        }

        /** Class logger */
        private readonly ILog log;

        /**
         * Cache of Methods, or CACHE_MISS, keyed by method
         * name and actual arguments used to find it.
         */
        private readonly IDictionary cache = new ConcurrentDictionary<string, object>();

        /** Map of methods that are searchable according to method parameters to find a match */
        private readonly MethodMap methodMap;

        private MethodCache(ILog log, TypeConversionHandler conversionHandler)
        {
            this.log = log;
            methodMap = new MethodMap(conversionHandler);
        }

        /**
         * Find a Method using the method name and parameter objects.
         *
         * Look in the methodMap for an entry.  If found,
         * it'll either be a CACHE_MISS, in which case we
         * simply give up, or it'll be a Method, in which
         * case, we return it.
         *
         * If nothing is found, then we must actually go
         * and introspect the method from the MethodMap.
         *
         * @param name The method name to look up.
         * @param params An array of parameters for the method.
         * @return A Method object representing the method to invoke or null.
         * @throws MethodMap.AmbiguousException When more than one method is a match for the parameters.
         */
        public Method get(String name, Object[] _params)
        {
            String methodKey = makeMethodKey(name, _params);

            Object cacheEntry = cache.get(methodKey);
            if (cacheEntry == CACHE_MISS)
            {
                // We looked this up before and failed.
                return null;
            }

            if (cacheEntry == null)
            {
                try
                {
                    // That one is expensive...
                    cacheEntry = methodMap.find(name, _params);
                }
                catch (MethodMap.AmbiguousException ae)
                {
                    /*
                     *  that's a miss :-)
                     */
                    cache.put(methodKey, CACHE_MISS);
                    throw ae;
                }

                cache.put(methodKey,
                        (cacheEntry != null) ? cacheEntry : CACHE_MISS);
            }

            // Yes, this might just be null.
            return (Method)cacheEntry;
        }

        private void put(Method method)
        {
            String methodKey = makeMethodKey(method);

            // We don't overwrite methods because we fill the
            // cache from defined class towards java.lang.Object
            // and that would cause overridden methods to appear
            // as if they were not overridden.
            if (cache.get(methodKey) == null)
            {
                cache.put(methodKey, method);
                methodMap.Add(method);
                if (debugReflection)
                {
                    log.Debug("Adding {}", method);
                }
            }
        }

        /**
         * Make a methodKey for the given method using
         * the concatenation of the name and the
         * types of the method parameters.
         *
         * @param method to be stored as key
         * @return key for ClassMap
         */
        private String makeMethodKey(Method method)
        {
            Class[] parameterTypes = method.getParameterTypes();
            int args = parameterTypes.Length;
            if (args == 0)
            {
                return method.getName();
            }

            StringBuilder methodKey = new StringBuilder((args + 1) * 16).Append(method.getName());

            foreach (Class parameterType in parameterTypes)
            {
                /*
                 * If the argument type is primitive then we want
                 * to convert our primitive type signature to the
                 * corresponding Object type so introspection for
                 * methods with primitive types will work correctly.
                 *
                 * The lookup map (convertPrimitives) contains all eight
                 * primitives (bool, byte, char, double, float, int, long, short)
                 * known to Java. So it should never return null for the key passed in.
                 */
                if (parameterType.isPrimitive())
                {
                    methodKey.Append((String)convertPrimitives.get(parameterType));
                }
                else
                {
                    methodKey.Append(parameterType.getName());
                }
            }

            return methodKey.ToString();
        }

        private String makeMethodKey(String method, Object[] _params)
        {
            int args = _params.Length;
            if (args == 0)
            {
                return method;
            }

            StringBuilder methodKey = new StringBuilder((args + 1) * 16).Append(method);

            foreach (Object arg in _params)
            {
                if (arg == null)
                {
                    methodKey.Append(NULL_ARG);
                }
                else
                {
                    methodKey.Append(arg.GetType().Name);
                }
            }

            return methodKey.ToString();
        }
    }
}
}